//
//  HermesContext.swift
//  MacDemo
//
//  Created by FH on 2020/12/2.
//  Copyright © 2020 FH. All rights reserved.
//

import Foundation

let KeyPreferences = "__preferences"
let KeyMainStates = "main"
let KeySettings = "settings"
let KeyCustom = "custom"
let KeyCell = "__cell"
fileprivate let mutex = Mutex()

class StreamContext : PropsStatesContext, Deinitable {
    deinit {
        HermesContext.states.removeValues(forSectionName: self.sectionName)
    }
    
    let streamId: UInt
    var dealloc = DeallocObserver()
    var isBinding: Bool {
        return true
    }
    var sectionName: String {
        return Self.getSectionName(self.streamId)
    }
    @PropState
    var isAudioUnmuted = false
    @PropState
    var isVideoUnmuted = false
    @PropState
    var isEnableDualMode = false
    
    init(_ streamId: UInt) {
        self.streamId = streamId
        self.bindStates(sectionName: self.sectionName, statesStore: HermesContext.states)
    }
    
    func toDictionary() -> [String : Any] {
        return HermesContext.states.toDictionary(sectionName: self.sectionName)
    }
    
    static func getSectionName(_ streamId: UInt) -> String {
        return "\(KeyCell).\(streamId)"
    }
    
    static func getKeyPath(_ keyPath: String, streamId: UInt) -> String {
        return "\(KeyCell).\(streamId).\(keyPath)"
    }
    
    static func parseCellKeyPathOfStateStore(_ key: String, _ sectionName: String) -> (Bool, UInt, String) {
        if sectionName.count > 0 && sectionName.starts(with: KeyCell) {
            let split = sectionName.components(separatedBy: ".")
            if split.count == 2, let streamId = UInt(split[1]) {
                return (true, streamId, key)
            }
        } else if key.starts(with: KeyCell) {
            let split = key.components(separatedBy: ".")
            if split.count >= 3, let streamId = UInt(split[1]) {
                let keyPath = key.getSubstring(from: KeyCell.count + split[1].count + 2)
                return (false, streamId, keyPath)
            }
        }
        
        return (false, 0, "")
    }
    
    static func parseCellStateOfScript(_ key: String) -> (forCell: Bool, forAll: Bool, keyPath: String) {
        let isCellKey = key.starts(with: "cell.") && key.count > 5
        let isCellsKey = key.starts(with: "cells.") && key.count > 6
        if isCellKey || isCellsKey {
            return (true, isCellsKey, key.getSubstring(from: isCellsKey ? 6 : 5))
        }
        return (false, false, "")
    }
}

final class StreamsInfo {
    fileprivate(set) var localStreamId: UInt = 0
    fileprivate(set) var remoteStreamIds = [UInt]()
    fileprivate(set) var contextMappings = [UInt : StreamContext]()
    
    func totalStreamsCount(_ useAspectFill: Bool) -> Int {
        return ((localStreamId > 0 && useAspectFill) ? 1 : 0) + self.remoteStreamIds.count
    }
    
    func contains(_ streamId: UInt) -> Bool {
        return self.localStreamId == streamId || self.remoteStreamIds.contains(streamId)
    }
    
    func getContext(_ streamId: UInt) -> StreamContext? {
        return self.contextMappings[streamId]
    }
    
    func addStream(_ streamId: UInt, forLocal: Bool) {
        let succ = mutex.tryLock()
        let ctx = StreamContext(streamId)
        self.contextMappings[streamId] = ctx
        if forLocal {
            self.localStreamId = streamId
        } else {
            self.remoteStreamIds.append(streamId)
        }
        if succ {
            mutex.unlock()
        }
    }
    
    func removeStream(_ streamId: UInt) {
        let succ = mutex.tryLock()
        if self.localStreamId == streamId {
            self.localStreamId = 0
        } else {
            if let index = self.remoteStreamIds.firstIndex(where: { $0 == streamId }) {
                self.remoteStreamIds.remove(at: index)
            }
        }
        self.contextMappings.removeValue(forKey: streamId)
        if succ {
            mutex.unlock()
        }
    }
    
    func removeAllRemoteStreams() {
        let succ = mutex.tryLock()
        for (key, _) in self.contextMappings {
            if key != self.localStreamId {
                self.contextMappings.removeValue(forKey: key)
            }
        }
        self.remoteStreamIds.removeAll()
        if succ {
            mutex.unlock()
        }
    }
    
    public func value(forKey: String, streamId: UInt) -> Any? {
        let keyPath = StreamContext.getKeyPath(forKey, streamId: streamId)
        return HermesContext.value(forKey: keyPath)
    }
    
    fileprivate func setValue(_ value: Any?, forKey: String, streamId: UInt?) {
        if let streamId = streamId {
            let keyPath = StreamContext.getKeyPath(forKey, streamId: streamId)
            HermesContext.setValue(value, forKey: keyPath)
        } else {
            self.setAllContextValue(value, forKey: forKey)
        }
    }
    
    fileprivate func setAllContextValue(_ value: Any?, forKey: String) {
        let succ = mutex.tryLock()
        for (streamId, _) in self.contextMappings {
            self.setValue(value, forKey: forKey, streamId: streamId)
        }
        if succ {
            mutex.unlock()
        }
    }
}

final class HermesMainStates : PropsStatesContext, Deinitable {
    var dealloc = DeallocObserver()
    var isBinding: Bool {
        return self.$appId.isBinding
    }
    // states for core sdk api
    @PropState
    var appId: AppAppIdType = ""
    @PropState
    var channelId: String = ""
    @PropState
    var token: String?
    @PropState
    var tokenType: AppTokenType = .static
    @PropState
    var channelProfile: AgoraChannelProfile = .communication
    @PropState(realType: AgoraClientRole.self)
    var clientRole: AgoraClientRole? = nil
    @PropState
    var localUID: UInt = 0
    // states for SDK logic
    @PropState
    var invokeMode: ScriptsInvokeMode = .manual
    @PropState
    var hasJoined = false
    @PropState
    var willJoinChannel = false
    @PropState
    var captureSharing = false
    @PropState
    var streamsInfo = StreamsInfo()
    
    // REMARK: inner states as trigger
    
    // type => (UID, forRemoteStream, forSetup)
    @PropState(autoReset:true)
    var handleStreamTrigger: (UInt, Bool, Bool)? = nil
    // type => (open/hide panel, stopScreen or updateParams)
    @PropState(autoReset:true)
    var showScreenCaptureTrigger: (Bool, Bool?) = (false, nil)
    @PropState(autoReset:true)
    var manualEnumCaptureSourcesTrigger = false
    @PropState(autoReset:true)
    var scriptEnumCaptureSourcesTrigger: Any? = nil
    @PropState
    var showCallActionLoadingTrigger = false
    
    // REMARK: inner appContext's actions
    
    @PropAction(onRespondsScriptAction)
    var actionRespondsScriptAction: String = ""
    // engine running & stop
    @PropAction(onEngineStartRunning)
    fileprivate var actionEngineRunning: Any = ""
    @PropAction(onEngineStop)
    fileprivate var actionEngineStop: Any = ""
    // local & remote streams
    @PropAction(onSetupLocalStream)
    fileprivate var actionSetupLocalStream: UInt = 0
    @PropAction(onSetupRemoteStream)
    fileprivate var actionSetupRemoteStream: UInt = 0
    @PropAction(onRemoveLocalStream)
    fileprivate var actionRemoveLocalStream: Any = ""
    @PropAction(onRemoveAllRemoteStreams)
    fileprivate var actionRemoveAllRemoteStreams: Any = ""
    @PropAction(onRemoveRemoteStream)
    fileprivate var actionRemoveRemoteStream: UInt = 0
    // screen captue
    @PropAction(onHandleStartCapture)
    fileprivate var actionHandleStartCapture = ""
}

extension HermesMainStates : RepositoriesStates {
    var sectionName: String {
        return KeyMainStates
    }
    
    func toDictionary() -> [String : Any] {
        return HermesContext.states.toDictionary(sectionName: KeyMainStates)
    }
}

extension AgoraSDKSettings : RepositoriesStates {
    var sectionName: String {
        return KeySettings
    }
    
    func toDictionary() -> [String : Any] {
        return HermesContext.states.toDictionary(sectionName: KeySettings)
    }
}

extension AppPreferences : RepositoriesStates {
    var sectionName: String {
        return KeyPreferences
    }
    
    func toDictionary() -> [String : Any] {
        return HermesContext.states.toDictionary(sectionName: KeyPreferences)
    }
}

final class HermesContext : PropsStatesContext, Deinitable {
    fileprivate static let states = StatesStore()
    private let localStates = HermesContext.states
    var dealloc = DeallocObserver()
    let preferences = AppPreferences()
    let mainStates = HermesMainStates()
    var settings = AgoraSDKSettings()
    // REMARK: states for environment init
    @PropState
    var isLinkedProject = false
    @PropState
    var linkedSDKVersion = ""
    @PropState
    var sdkVersion = ""
    @PropState
    var scriptsInvoker: ScriptsInvoker = ScriptsInvoker()
    @PropState
    var engine: ScriptEngine!
    @PropState
    var apisTags: APIsTagsHandler!
    @PropState
    var privateParams: [String : AstNode]!
    // REMARK: states for engine & invoker init
    @PropState
    var loadScript = ""
    // save apis.json & params.json
    @PropState
    var loadAPIsPath = ""
    @PropState
    var loadParamsPath = ""
    @PropState(autoReset:true)
    var loadScriptTrigger = false
    @PropState
    var isInited = false
    @PropState
    var isEngineRunning = false
    var agoraSDK: AgoraRtcEngineKit? {
        if let engine = self.engine, engine.isRunning, let sdk = engine.invokeTarget as? AgoraRtcEngineKit {
            return sdk
        }
        return nil
    }
    @PropState
    var resourcePathInBundle = ""
    @PropState
    var appTag = ""
    @PropState
    var settingsUseMode: AgoraSDKSettingsUseMode = .notUsed
    @PropState
    var settingsByScriptName: String = ""
    @PropState
    var settingsByScript: [String : Any]? = nil
   
    init() {
        // local config states
        var resourcePath = ""
        var tag = ""
        var isProject = false
        var linkedVersion = ""
        var existsPreferences = false
        
        let sectionNameMapping = ["preferences" : KeyPreferences, "mainStates": KeyMainStates]
        // bind states
        let isInited = self.localStates.getState("isInited", sectionName: "")
        if isInited == nil {
            // register value encoder
            ScriptEncoder.register({[weak self] (forEncode, typeName, value) -> Any? in
                if forEncode {
                    if let _ = value as? () {
                        return "Void"
                    } else if typeName.hasSuffix(".CGFloat") {
                        return "\(value)"
                    } else if typeName.hasPrefix("__C.") {
                        if typeName.hasSuffix(".CGSize") {
                            let size = value as! CGSize
                            return "(\(size.width),\(size.height))"
                        } else if typeName.hasSuffix(".CGPoint") {
                            let point = value as! CGPoint
                            return "(\(point.x),\(point.y))"
                        } else if typeName.hasSuffix(".CGRect") {
                            let rect = value as! CGRect
                            return "(\(rect.origin.x),\(rect.origin.y),\(rect.size.width),\(rect.size.height))"
                        } else {
                            return self?.engine.getEnumValue(typeName, value: value)
                        }
                    } else if let complexEnumValue = value as? RawEnumConvertible {
                        return complexEnumValue.anyRawValue
                    } else if typeName.hasPrefix("Agora"), let engine = self?.engine,
                        let node = engine.findAstNode(by: typeName), node.type == .interface,
                        let jsonStr = dumpAnyToJsonString(forSerialization: true,
                                                          valueOrNil: value,
                                                          node: node,
                                                          engine: engine) {
                        return jsonStr
                    } else {
                        return value
                    }
                } else {
                    if let valueStr = value as? String {
                        if valueStr == "Void" {
                            return ()
                        } else if typeName.hasSuffix(".CGFloat"), let value = Float(valueStr) {
                            return CGFloat(value)
                        } else if typeName.hasPrefix("__C."), let engine = self?.engine {
                            let structValue = valueStr.trim("(", ")").components(separatedBy: ",")
                            if valueStr.hasPrefix("(") && valueStr.hasSuffix(")") {
                                if typeName.hasSuffix(".CGSize") && structValue.count == 2 {
                                    return CGSize(width: Float(structValue[0])!, height: Float(structValue[1])!)
                                } else if typeName.hasSuffix(".CGPoint") && structValue.count == 2 {
                                    return CGPoint(x: Float(structValue[0])!, y: Float(structValue[1])!)
                                } else if typeName.hasSuffix(".CGRect") && structValue.count == 4 {
                                    return CGRect(x: Float(structValue[0])!, y: Float(structValue[1])!,
                                                  width: Float(structValue[2])!, height: Float(structValue[3])!)
                                }
                            } else {
                                return castStringToEnumValue(typeName, valueStr, engine: engine)
                            }
                        } else if typeName.hasPrefix("Hermes.") {
                            let split = typeName.components(separatedBy: ".")
                            if let complexEnumType = allComplexEnumTypes[split[1]] as? RawEnumConvertible.Type {
                                return complexEnumType.createInstance(by: valueStr)
                            }
                        } else if typeName.hasPrefix("Agora"), let engine = self?.engine,
                            let node = self?.engine.findAstNode(by: typeName), node.type == .interface {
                            return castJsonStringToModel(valueStr, modelType: typeName, engine: engine)
                        }
                    }
                }
                return nil
            })
            // parse config.json
            if let configPath = Bundle.main.path(forResource: "config", ofType: "json"),
                let configStr = try? String(contentsOfFile: configPath),
                let data = configStr.data(using: .utf8),
                let config = try? JSONDecoder().decode([String : String].self , from: data) {
                resourcePath = configPath.getSubstring(to: configPath.count - "/config.json".count)
                tag = config["tag"] ?? ""
                if let value = config["isLinkedProject"], let result = Bool(value) {
                    isProject = result
                }
                if let result = config["linkedSDKVersion"] {
                    linkedVersion = result
                }
            }
            // load preferences from local cache
            let preferencesName = (tag.count > 0 ? "\(tag)_" : "") + "preferences.json"
            let preferencesPath = NSHomeDirectory() + "/Library/Caches/Hermes_Mac/\(preferencesName)"
            existsPreferences = FileManager.default.fileExists(atPath: preferencesPath)
            var encoder = [String : String]()
            if FileManager.default.fileExists(atPath: preferencesPath),
               let preferencesStr = try? String(contentsOfFile: preferencesPath),
               let data = preferencesStr.data(using: .utf8) {
                encoder = try! JSONDecoder().decode([String : String].self, from: data)
            }
            let preferences = AppPreferences.decode(encoder)
            // bind for init
            self.bindStates(mappingSections: sectionNameMapping, initState: (KeyPreferences, preferences))
            self.$isInited.update(value: true, self.localStates)
            // observer preferences's value change
            self.localStates.subscribeState(byKey: "", sectionName: KeyPreferences).subscribeNext {[weak self] _ in
                if let self = self {
                    let preferences = self.preferences.toDictionary()
                    let encoder = AppPreferences.encode(preferences)
                    let data = try! JSONEncoder().encode(encoder)
                    let jsonStr = String(data: data, encoding: .utf8)
                    try! jsonStr?.write(toFile: preferencesPath, atomically: true, encoding: .utf8)
                }
            }
            // observer main's value change
            self.localStates.subscribeState(byKey: "", sectionName: KeyMainStates).subscribeNext {[weak self] (value, key, _) in
                if let self = self {
                    if key == "channelProfile", let nativeValue = value as? Int, nativeValue == AgoraChannelProfile.game.rawValue {
                        self.mainStates.$channelProfile.update(value: .liveBroadcasting, HermesContext.states)
                        self.mainStates.$clientRole.update(value: .broadcaster, HermesContext.states)
                        self.mainStates.$appId.update(value: .staticBroadcasting, HermesContext.states)
                        self.mainStates.$tokenType.update(value: .static, HermesContext.states)
                    }
                }
            }
            // log setting
            Log.logger.directory = getAppRootPath() + "/Logs"
            Log.logger.name = "hermes"
            Log.logger.maxFileSize = 4096
            Log.logger.maxFileCount = 4096
            Log.logger.printToConsole = false
            if existsPreferences {
                __logWrite("appTag: \(tag), no create new preferences file")
            } else {
                let cacheDir = NSHomeDirectory() + "/Library/Caches/Hermes_Mac"
                var oldTag = ""
                if resourcePath.count > 0,
                    let files = try? FileManager.default.contentsOfDirectory(atPath: cacheDir) {
                    for file in files {
                        if file.hasSuffix("preferences.json") {
                            let split = file.components(separatedBy: "_")
                            if split.count == 2 {
                                let currTag = split[0]
                                if currTag > oldTag {
                                    oldTag = currTag
                                }
                            }
                        }
                    }
                }
                __logWrite("appTag: \(tag), create new preferences file from: \(oldTag)")
            }
            // register actions
            self.registerActions()
        } else {
            // bind for monoStates pattern
            self.bindStates(mappingSections: sectionNameMapping)
        }
        // init engine & scriptInvoker
        if sdkVersion.count == 0 && self.engine == nil {
            // save config states
            self.$resourcePathInBundle.update(value: resourcePath, self.localStates)
            self.$appTag.update(value: tag, self.localStates)
            self.$isLinkedProject.update(value: isProject, self.localStates)
            if linkedVersion.count > 0 {
                self.$linkedSDKVersion.update(value: linkedVersion, self.localStates)
                if isProject {
                    self.$sdkVersion.update(value: linkedVersion, self.localStates)
                }
            }
            
            if !self.isLinkedProject {
                self.$sdkVersion.update(value: AgoraRtcEngineKit.getSdkVersion(), self.localStates)
            }
            // load apis & params
            self.loadScriptEngine(nil)
            // load current script
            self.loadScriptBundles(nil)
        }
    }
    
    // REMARK: basic methods
    
    public func updateSettingsStatas(_ settings: [String : Any]) -> Bool {
        if let info = self.preferences.currScriptInfo {
            let folder = NSHomeDirectory() + "/Library/Caches/Hermes_Mac/Settings/"
            let manualSettingsName = "\(self.appTag)_\(info.name)_\(info.type.rawValue)_settings.json"
            let manualSettingsPath = folder + "\(manualSettingsName)"
            if !FileManager.default.fileExists(atPath: folder) {
                try! FileManager.default.createDirectory(atPath: folder,
                                                         withIntermediateDirectories: true,
                                                         attributes: nil)
            }
            // build byScript dict
            var store = StatesStore()
            let _ = AgoraSDKSettings(store, initState: (self.settingsByScript ?? [:]))
            let scriptDict = store.toDictionary(sectionName: "settings")
            // build default dict and detect
            store = StatesStore()
            let result = AgoraSDKSettings(store).detectUpdated(store: store, scriptDict, settings)
            // update by manual
            if result.1 {
                let encoder = AgoraSDKSettings.encode(settings)
                if let data = try? JSONEncoder().encode(encoder),
                   let settingsStr = String(data: data, encoding: .utf8) {
                    try! settingsStr.write(toFile: manualSettingsPath, atomically: true, encoding: .utf8)
                }
                self.settings.resetStates(forUpdate: true, self.localStates, settings)
                self.$settingsUseMode.update(value: .usedByManual, self.localStates)
                return true
            } else {
                if FileManager.default.fileExists(atPath: manualSettingsPath) {
                    try! FileManager.default.removeItem(atPath: manualSettingsPath)
                }
                if result.0 {
                    // update by script
                    if self.settingsUseMode != .usedByScript, let settings = self.settingsByScript {
                        self.settings.resetStates(forUpdate: true, self.localStates, settings)
                        self.$settingsUseMode.update(value: .usedByScript, self.localStates)
                        return true
                    }
                } else {
                    // not set settings
                    if self.settingsUseMode != .notUsed {
                        self.settings.resetStates(forUpdate: true, self.localStates)
                        self.$settingsUseMode.update(value: .notUsed, self.localStates)
                        return true
                    }
                }
            }
        }
        return false
    }
    
    public func bindTo<ControlType: NSView>(keyPath: String, _ control: ControlType,
                                            _ handler: @escaping (ControlType, Any) -> ()) {
        statesBindTo(self.localStates, keyPath: keyPath, control, handler)
    }
    
    public func subscribe<ControlType: NSView>(keyPath: String, _ control: ControlType, _ mappingControl: StateMappingControl) {
        statesSubscribeFrom(self.localStates, self.engine, keyPath: keyPath, control,
                            mappingControl, setValueFn: self.setValue)
    }

    public func subscribe(_ predicate: ScriptDirective, _ btn: NSButton, aliasName: String) {
        if case .predicateContext(let key, _, let predicateValue, _) = predicate {
            let (isCustom, handledKeyPath, _) = self.getStateKeyPath(key)
            var signal = self.localStates.subscribeState(byKey: handledKeyPath, sectionName: "")
            let startValue = self.localStates.getState(handledKeyPath, sectionName: "")
            if let boolValue = predicateValue as? Bool {
                if startValue == nil && !boolValue {
                    // set initValue for customState use in .predicate
                    if isCustom {
                        self.setValue(boolValue, forKey: handledKeyPath)
                    }
                    signal = signal.start(with: (boolValue, "", ""))
                } else if let stateValue = startValue as? Bool, stateValue == boolValue {
                    signal = signal.start(with: (boolValue, "", ""))
                }
            }
            signal.take(until: btn.rac_willDealloc).subscribeNext {[weak btn] (val, key, sectionName) in
                if equalsOptionalAny(lhs: predicateValue, rhsValue: val) {
                    btn?.flTitle = aliasName.trimString()
                }
            }
        }
    }

    public func value(forKey: String) -> Any? {
        return self.localStates.getState(forKey, sectionName: "")
    }
    
    public static func value(forKey: String) -> Any? {
        return self.states.getState(forKey, sectionName: "")
    }
    
    fileprivate func hasValue(forKey: String) -> Bool {
        return self.localStates.hasState(forKey, sectionName: "")
    }
    
    fileprivate func setValue(_ value: Any?, forKey: String) {
        if let value = value {
            self.localStates.updateState(value as Any, byKey: forKey, sectionName: "")
        } else {
            self.localStates.updateState(value as Any, byKey: forKey, sectionName: "")
        }
    }
    
    fileprivate static func setValue(_ value: Any?, forKey: String) {
        if let value = value {
            self.states.updateState(value as Any, byKey: forKey, sectionName: "")
        } else {
            self.states.updateState(value as Any, byKey: forKey, sectionName: "")
        }
    }
    
    fileprivate func setValue<T>(value: T, forKey: String) {
        self.localStates.updateState(value as Any, byKey: forKey, sectionName: "")
    }
    
    fileprivate func predicateValue(_ value: Any?, forKey: String) -> Bool {
        if let contextValue = self.localStates.getState(forKey, sectionName: "") {
            return equalsOptionalAny(lhs: value, rhsValue: contextValue)
        }
        return false
    }
    
    fileprivate func setContextOrCellState(_ value: Any?, _ handledKeyPath: String, _ cellResult: Bool?, _ contextStreamId: UInt) {
        var forCell = cellResult != nil
        let forCells = forCell && cellResult! == false
        let streamId: UInt? = forCells ? nil : contextStreamId
        forCell = streamId == nil || streamId! > 0
        if forCell {
            self.mainStates.streamsInfo.setValue(value, forKey: handledKeyPath, streamId: streamId)
        } else {
            self.setValue(value, forKey: handledKeyPath)
        }
    }
    
    // detect state belongs appContext or custom.States。@forCell: true is forCell, false is forCells
    fileprivate func getStateKeyPath(_ key: String) -> (isCustom: Bool, keyPath: String, cellResult: Bool?) {
        let cellResult = StreamContext.parseCellStateOfScript(key)
        if cellResult.forCell {
            return (false, cellResult.keyPath, cellResult.forAll ? false : true)
        } else {
            let mainPrefix = "\(KeyMainStates)."
            let settingsPrefix = "\(KeySettings)."
            let customPrefix = "\(KeyCustom)."
            if key.starts(with: mainPrefix) || key.starts(with: settingsPrefix) {
                return (false, key, nil)
            } else if key.starts(with: customPrefix) || key.contains(".") {
                return (true, key, nil)
            } else {
                var keyPath = "\(mainPrefix)\(key)"
                if self.hasValue(forKey: keyPath) {
                    return (false, keyPath, nil)
                }
                keyPath = "\(settingsPrefix)\(key)"
                if self.hasValue(forKey: keyPath) {
                    return (false, keyPath, nil)
                }
                keyPath = key
                if self.hasValue(forKey: keyPath) {
                    return (false, keyPath, nil)
                }
            }
            return (true, "\(customPrefix)\(key)", nil)
        }
    }

    fileprivate func getStateInitValue(_ initValue: Any, _ control: StateMappingControl?, _ referenceValue: Any?) -> Any {
        // handle caseIndex => enumValue
        if let valueStr = initValue as? String, valueStr.starts(with: "__Case:") {
            let split = valueStr.components(separatedBy: " ")
            if split.count == 2, let caseIndex = Int(split[1]), caseIndex >= 0 {
                if let control = control, control.type == .dropbox, (control.enumClass != nil || control.stringItems != nil) {
                    if let items = control.stringItems {
                        return items[caseIndex]
                    } else if let enumClass = control.enumClass,
                              let enumValue = getEnumAnyValue(by: caseIndex, enumClass, self.engine) {
                        return enumValue
                    }
                } else if let value = referenceValue,
                          let complexEnumType = type(of: value) as? ComplexCaseIterable.Type,
                          let enumValue = getEnumAnyValue(by: caseIndex, "\(complexEnumType)", self.engine) {
                    return enumValue
                }
            }
        }
        return initValue
    }
    
    fileprivate func registerActions() {
        let mirror = Mirror(reflecting: self.mainStates)
        for item in mirror.children {
            if let action = item.value as? PropActionBindable {
                action.startMonitoring(self, self.localStates)
            }
        }
    }
    
    func toDictionary() -> [String : Any] {
        return self.localStates.toDictionary()
    }
    
    func toDictionarySelf() -> [String : Any] {
        var dict = [String : Any]()
        for (key, value) in localStates.toDictionary() {
            if !key.contains(".") {
                dict[key] = value
            }
        }
        return dict
    }
    
    // REMARK: engine、invoker init methods

    func scanScriptList(customScriptsPath: String?) -> [String] {
        var needUpdate = false
        var result: [String]! = nil
        autoreleasepool {
            var scriptInfos = self.preferences.scriptInfos
            // scan new script file
            let scanScripts = { (dir: String, inBundle: Bool) in
                if let files = try? FileManager.default.contentsOfDirectory(atPath: dir) {
                    for file in files {
                        if file.hasSuffix("script") {
                            let path = "\(dir)/\(file)"
                            if scriptInfos[path] == nil {
                                needUpdate = true
                                let hash = path.fileHash() ?? ""
                                if file == "default.script" {
                                    scriptInfos[path] = AppPreferences.ScriptInfo(inBundle: inBundle,
                                                                                  name: ScriptBundleType.default.rawValue,
                                                                                  fileHash: hash, type: .default)
                                } else {
                                    let (name, type) = ScriptParser(path, self.engine).scan()
                                    scriptInfos[path] = AppPreferences.ScriptInfo(inBundle: inBundle, name: name,
                                                                                  fileHash: hash, type: type)
                                }
                            }
                        }
                    }
                }
            }
            // detect file be removed
            for path in scriptInfos.allKeys {
                if !FileManager.default.fileExists(atPath: path) {
                    needUpdate = true
                    scriptInfos.removeValue(forKey: path)
                }
            }
            // scan NSBundle
            if let dir = Bundle.main.resourcePath {
                scanScripts(dir, true)
            }
            // scan custom path
            if let path = customScriptsPath, self.preferences.customScriptsLocation != path {
                self.preferences.$customScriptsLocation.update(value: path, self.localStates)
            }
            if let dir = self.preferences.customScriptsLocation {
                scanScripts(dir, false)
            }
            if needUpdate {
                self.preferences.$scriptInfos.update(value: scriptInfos, self.localStates)
            }
            
            var items = [(ScriptBundleType, String)]()
            for (_, info) in self.preferences.scriptInfos {
                if info.type != .default {
                    items.append((info.type, info.name))
                }
            }
            items.sort { (item1, item2) in
                let compare = (item1.0, item2.0)
                switch compare {
                case (.scenario, .scenario), (.custom, .custom):
                    return item1.1 < item2.1
                case (.scenario, .custom):
                    return false
                default:
                    return true
                }
            }
            result = items.map({ "\($0.0)-\($0.1)" })
            result.insert(ScriptBundleType.default.rawValue, at: 0)
        }
        return result
    }
    
    func loadScriptBundles(_ scriptName: String?) {
        if self.loadScript != "" && scriptName == nil {
            return
        }
        var scriptInfos = self.preferences.scriptInfos
        // get script name
        var bundleType: ScriptBundleType = .default
        var filePath = ""
        var fileHash = ""
        var forceLoad = true
        var currName = ScriptBundleType.default.rawValue
        if let scriptName = scriptName {
            let split = scriptName.components(separatedBy: "-")
            if split.count == 2 {
                if let type = ScriptBundleType(rawValue: split[0]) {
                    bundleType = type
                }
                currName = split[1]
            } else {
                currName = ScriptBundleType.default.rawValue
            }
        } else if self.preferences.currScript.count > 0 {
            currName = self.preferences.currScript
        }
        // check need to force load script
        for (path, info) in scriptInfos {
            if currName == info.name {
                filePath = path
                fileHash = path.fileHash() ?? ""
                bundleType = info.type
                if fileHash == info.fileHash {
                    forceLoad = false
                }
                break
            }
        }
        if self.loadScript == currName && !forceLoad { // TODO: remove?
            return
        }
        if self.loadScript != "" {
            // remove settings byScript and byManual
            self.localStates.removeValues(forSectionName: KeySettings)
            self.settings.resetStates(forUpdate: false, self.localStates)
            self.$settingsByScript.update(value: nil, self.localStates)
            self.$settingsByScriptName.update(value: "", self.localStates)
            self.$settingsUseMode.update(value: .notUsed, self.localStates)
            
            self.mainStates.resetStates(forUpdate: false, self.localStates)
        }
        self.preferences.$currScript.update(value: currName, self.localStates)
        
        // load script
        let fileName = (bundleType == .default) ? bundleType.rawValue : "\(bundleType.rawValue)_\(currName)"
        if !self.preferences.currScript.isEmpty {
            // load cached json file
            var bundles: ScriptBundles! = nil
            if !forceLoad {
                bundles = ScriptBundles.loadFromCache(name: (bundleType == .default) ? "" : currName, type: bundleType)
            }
            if bundles == nil {
                if filePath.count == 0 {
                    // manual handle path for xxx.script file
                    let bundlePath = "\(self.resourcePathInBundle)\(fileName).script"
                    if FileManager.default.fileExists(atPath: bundlePath) {
                        filePath = bundlePath
                    } else if let customPath = self.preferences.customScriptsLocation {
                        filePath = "\(customPath)/\(fileName).script"
                    }
                }
                do {
                    bundles = try ScriptParser(filePath, self.engine).parse()
                    // save json file
                    bundles?.writeToLocalJson()
                    if let item = scriptInfos[filePath], forceLoad {
                        scriptInfos[filePath] = AppPreferences.ScriptInfo(inBundle: item.inBundle, name: item.name,
                                                                            fileHash: fileHash, type: item.type)
                        self.preferences.$scriptInfos.update(value: scriptInfos, self.localStates)
                    }
                } catch let errMessage {
                    FLShowCloseAlert("load script \(fileName) fail: \(errMessage)")
                }
            }
            if let bundles = bundles {
                self.$loadScript.update(value: self.preferences.currScript, self.localStates)
                self.scriptsInvoker.loadBundles(bundles)
                self.$loadScriptTrigger.update(value: true, self.localStates)
            }
        }
    }
    
    /*
     * @param setStatesForSettings: set states for setting not fetch infos
     * @param filterSettings: only fetch settings infos
     * @result: [StateKeyPath, aliasName, (state=true, predicate=false, action=nil), mappingControl]
     */
    func getCustomMappingControls(setStatesForSettings: Bool,
                                  filterSettings: Bool) -> [(String, String, Bool?, StateMappingControl?)] {
        let settingsPrefix = "\(KeySettings)."
        var items = [(String, String, Bool?, StateMappingControl?)]()
        for (key, (forPredicate, initValue, mappingControl)) in self.scriptsInvoker.getAllStatesAndPredicates() {
            let (isCustom, keyPath, cellResult) = self.getStateKeyPath(key)
            // ignore action/states for grid cell
            if cellResult != nil {
                continue
            }
            // detect settings state use or not
            let isSettingsState = keyPath.starts(with: settingsPrefix)
            let canSetInitValue = (isSettingsState && setStatesForSettings) || !isSettingsState
            let settingsStateKey = isSettingsState ? keyPath.components(separatedBy: ".")[1] : ""
            // set init value
            if let value = initValue, canSetInitValue {
                let realValue = self.getStateInitValue(value, mappingControl, self.value(forKey: keyPath))
                self.setValue(realValue, forKey: keyPath)
                // save setting by script
                if isSettingsState {
                    var byScript: [String : Any] = self.settingsByScript ?? [:]
                    byScript[settingsStateKey] = realValue
                    self.$settingsByScript.update(value: byScript, self.localStates)
                }
            }
            if (filterSettings && !isSettingsState) || (!filterSettings && isSettingsState) {
                continue
            }
            // get aliasName
            if (isCustom || filterSettings), let control = mappingControl {
                var aliasName = control.aliasName
                if aliasName.isEmpty {
                    let split = key.components(separatedBy: ".")
                    if split.count == 2 {
                        aliasName = split[1]
                    } else {
                        aliasName = key
                    }
                }
                items.append((keyPath, aliasName, forPredicate, control))
            }
        }
        if !filterSettings {
            for (action, arguments) in self.scriptsInvoker.getCustomActions() {
                var aliasName = action
                let split = action.components(separatedBy: ".")
                if split.count == 2 {
                    aliasName = split[1]
                }
                var control: StateMappingControl? = nil
                if let arguments = arguments {
                    control = StateMappingControl(.none, "")
                    control!.associatedActionArguments = arguments
                }
                items.append((action, aliasName, nil, control))
            }
        }
        
        // detect sdkSettings used by script or manual
        var manualDict: [String : Any]? = nil
        let info = self.preferences.currScriptInfo ?? AppPreferences.ScriptInfo(inBundle: true, name: "default", fileHash: "", type: .default)
        if info.name != self.settingsByScriptName {
            self.$settingsByScriptName.update(value: info.name, self.localStates)
            let folder = NSHomeDirectory() + "/Library/Caches/Hermes_Mac/Settings/"
            let manualSettingsName = "\(self.appTag)_\(info.name)_\(info.type.rawValue)_settings.json"
            let manualSettingsPath = folder + "\(manualSettingsName)"
            if FileManager.default.fileExists(atPath: manualSettingsPath),
               let settingsStr = try? String(contentsOfFile: manualSettingsPath),
               let data = settingsStr.data(using: .utf8) {
                let encoder = try! JSONDecoder().decode([String : String].self, from: data)
                manualDict = AgoraSDKSettings.decode(encoder, store: self.localStates)
            }
            
            let scriptDict = self.settings.toDictionary().mergeValues(by: (self.settingsByScript ?? [:]))
            let store = StatesStore()
            let (byScript, byManual) = AgoraSDKSettings(store).detectUpdated(store: store, scriptDict, manualDict)
            if byManual, let dict = manualDict {
                self.settings.resetStates(forUpdate: false, self.localStates, dict)
                self.$settingsUseMode.update(value: .usedByManual, self.localStates)
            } else if byScript {
                self.$settingsUseMode.update(value: .usedByScript, self.localStates)
            } else {
                self.$settingsUseMode.update(value: .notUsed, self.localStates)
                self.$settingsByScript.update(value: nil, self.localStates)
            }
        }
        return items
    }
    
    func checkAPIsNParamsPaths(_ path: String, forParams: Bool) -> Bool {
        var needUpdate = false
        let url = URL(fileURLWithPath: path)
        if forParams {
            let result = "\(self.sdkVersion)_params.json"
            if url.lastPathComponent == "params.json" {
                self.preferences.$scanParamsLocation.update(value: path, self.localStates)
                self.$loadParamsPath.update(value: path, self.localStates)
                needUpdate = true
            } else {
                if result == url.lastPathComponent {
                    self.preferences.$scanParamsLocation.update(value: path, self.localStates)
                    self.$loadParamsPath.update(value: path, self.localStates)
                    needUpdate = true
                }
            }
        } else {
            let result = "\(self.sdkVersion)_full_apis.json"
            if url.lastPathComponent == "apis.json" {
                self.preferences.$scanAPIsLocation.update(value: path, self.localStates)
                self.$loadAPIsPath.update(value: path, self.localStates)
                needUpdate = true
            } else {
                if result == url.lastPathComponent {
                    self.preferences.$scanAPIsLocation.update(value: path, self.localStates)
                    self.$loadAPIsPath.update(value: path, self.localStates)
                    needUpdate = true
                }
            }
        }
        if needUpdate {
            let agoraKitHeaderPath = Bundle.main.path(forResource: "AgoraRtcEngineKit", ofType: "header")!
            self._loadScriptEngine(self.loadAPIsPath, agoraKitHeaderPath, self.loadParamsPath)
        }
        return needUpdate
    }
    
    private func getAPIsNParamsPaths(_ scanAPIsPath: String?) -> (String, String, String) {
        if let scanAPIsPath = scanAPIsPath {
            self.$loadAPIsPath.update(value: "", self.localStates)
            self.$loadParamsPath.update(value: "", self.localStates)
            self.preferences.$scanAPIsLocation.update(value: scanAPIsPath, self.localStates)
        }
        // get custom dir
        var customAPIsDir = ""
        var customParamsDir = ""
        if let customDir = self.preferences.scanAPIsLocation {
            customAPIsDir = customDir
        }
        if let customDir = self.preferences.scanParamsLocation {
            customParamsDir = customDir
        }
        // get path of apis & params，todo remove ‘voice’ and 'full' suffix
        let agoraKitHeaderPath = Bundle.main.path(forResource: "AgoraRtcEngineKit", ofType: "header")!
        var apisFilePath = "\(customAPIsDir)/\(self.sdkVersion)_full_apis.json"
        var cppPrivateParamsPath = "\(customParamsDir)/\(self.sdkVersion)_params.json"
        // load from custom path of api.json
        if FileManager.default.fileExists(atPath: apisFilePath) {
            self.$loadAPIsPath.update(value: apisFilePath, self.localStates)
        } else {
            // load api.json from bundle
            if let path = Bundle.main.path(forResource: "\(self.sdkVersion)_full_apis", ofType: "json"),
               FileManager.default.fileExists(atPath: path) {
                apisFilePath = path
                self.$loadAPIsPath.update(value: apisFilePath, self.localStates)
            } else if let path = Bundle.main.path(forResource: "apis", ofType: "json"),
               FileManager.default.fileExists(atPath: path) {
                apisFilePath = path
                self.$loadAPIsPath.update(value: apisFilePath, self.localStates)
            }
        }
        // load from custom path of params.json
        if FileManager.default.fileExists(atPath: cppPrivateParamsPath) {
            self.$loadParamsPath.update(value: cppPrivateParamsPath, self.localStates)
        } else {
            // load api.json from bundle
            if let path = Bundle.main.path(forResource: "\(self.sdkVersion)_params", ofType: "json"),
               FileManager.default.fileExists(atPath: path) {
                cppPrivateParamsPath = path
                self.$loadParamsPath.update(value: cppPrivateParamsPath, self.localStates)
            } else if let path = Bundle.main.path(forResource: "params", ofType: "json"),
               FileManager.default.fileExists(atPath: path) {
                cppPrivateParamsPath = path
                self.$loadParamsPath.update(value: cppPrivateParamsPath, self.localStates)
            }
        }
        return (apisFilePath, agoraKitHeaderPath, cppPrivateParamsPath)
    }
    
    func loadScriptEngine(_ scanAPIsPath: String?) {
        let (pathOfAPIs, pathOfHeader, pathOfParams) = self.getAPIsNParamsPaths(scanAPIsPath)
        self._loadScriptEngine(pathOfAPIs, pathOfHeader, pathOfParams)
    }
    
    func _loadScriptEngine(_ pathOfAPIs: String, _ pathOfHeader: String, _ pathOfParams: String) {
        // parse ObjC nodes
        let parser = APIsParser(keyWord: "Agora", jsonFilePath: pathOfAPIs)
        var nodes = parser.parse()
        let apisTags = APIsTagsHandler(astNodes: nodes,
                                       objcAPIsHeaderPath: pathOfHeader,
                                       apiClassName: "AgoraRtcEngineKit",
                                       delegateClassName: "AgoraRtcEngineDelegate")
        self.$apisTags.update(value: apisTags, self.localStates)
        nodes = self.apisTags.parse()
        // create instance for AgoraSDK
        if self.engine != nil {
            self.engine.removeTarget(target: self)
        }
        let engine = ScriptEngine(astNodes: nodes, apiClassName: "AgoraRtcEngineKit", delegateClassNames: [])
        self.$engine.update(value: engine, self.localStates)
        engine.setEngineName("Hermes")
        // handle set value of readonly property
        engine.registerSetPropertiesHandler { (modelType, instance, propName, value) in
            if modelType == "AgoraChannelMediaRelayConfiguration" && propName == "destinationInfos",
                let model = instance as? AgoraChannelMediaRelayConfiguration {
                if let value = value, let infos = value as? [String : AgoraChannelMediaRelayInfo] {
                    for (key, dest) in infos {
                        model.setDestinationInfo(dest, forChannelName: key)
                    }
                }
                return true
            }
            return false
        }
        engine.addTarget(target: self, runInMainQueue: true)
        // try to load params
        self.loadPrivateParams(pathOfParams)
        // register invocation
        self.registerScriptInvocation()
    }
    
    func loadPrivateParams(_ pathOfParams: String) {
        if pathOfParams != "" {
            let paramsParser = APIsCPPParser(namespaceRootKey: "agora", dumpFilePath: nil, jsonFilePath: pathOfParams)
            let privateParamNodes = paramsParser.parse()
            self.$privateParams.update(value: privateParamNodes, self.localStates)
        }
    }
   
    func registerScriptInvocation() {
        let sdkInvocation = InstructionSDKInvocation(engine: self.engine) {[weak self] (directive, mode, instrContext) in
            if let self = self {
                switch directive {
                case .contextArgToken:
                    return self.fetchToken()
                case .contextStates(let keyPath, let initValue, let control):
                    let (_, handledKeyPath, _) = self.getStateKeyPath(keyPath)
                    var value = instrContext[keyPath]
                    if value == nil {
                        value = self.value(forKey: handledKeyPath)
                    }
                    // force set initValue in .automatic mode and use as `defaultValue` in .semiAutomatic mode
                    if value == nil || mode == .automatic, let initValue = initValue {
                        value = self.getStateInitValue(initValue, control, self.value(forKey: handledKeyPath))
                        self.setValue(value, forKey: handledKeyPath)
                    }
                    if let enumValue = value as? RawEnumConvertible {
                        return enumValue.anyRawValue
                    } else {
                        return value
                    }
                case .predicateContext(let keyPath, let compareOp, let value, _):
                    let checkPredicateFn: Function3<String, Any?, Any, Any?> = { (op, v, state) in
                        var equalsResult = equalsOptionalAny(lhs: v, rhsValue: state)
                        if let rawEnum = state as? RawEnumConvertible {
                            equalsResult = equalsOptionalAny(lhs: v, rhsValue: rawEnum.anyRawValue)
                        }
                        if compareOp == "=" {
                            return equalsResult
                        } else if compareOp == "!=" {
                            return !equalsResult
                        } else {
                            if let value = v {
                                if let lhsValue = state as? TypeComparerible, let rhsValue = value as? TypeComparerible {
                                    if compareOp == ">" {
                                        return lhsValue.greater(rhs: rhsValue)
                                    } else if compareOp == ">=" {
                                        return lhsValue.greaterAndEquals(rhs: rhsValue)
                                    } else if compareOp == "<" {
                                        return lhsValue.less(rhs: rhsValue)
                                    } else if compareOp == "<=" {
                                        return lhsValue.lessAndEquals(rhs: rhsValue)
                                    }
                                } else {
                                    let valueType = getAnyValueType(state)
                                    fatalError("\(compareOp) not support type: \(valueType)")
                                }
                            }
                        }
                        return false
                    }
                    if let state = instrContext[keyPath] {
                        return checkPredicateFn(compareOp, value, state)
                    } else {
                        let (_, handledKeyPath, _) = self.getStateKeyPath(keyPath)
                        if let state = self.localStates.getState(handledKeyPath, sectionName: "") {
                            return checkPredicateFn(compareOp, value, state)
                        } else if let asState = value {
                            // keyPath is custom state and not set init value
                            return checkPredicateFn(compareOp, nil, asState)
                        }
                    }
                    return false
                case .predicateResponds(let methodName, let className, let compareOp):
                    let result = self.engine.responds(methodName, className: className)
                    if compareOp == "!=" {
                        return !result
                    } else {
                        return result
                    }
                case .contextCallAction(let methodName, let args):
                    var arg: Any? = nil
                    let (_, handledKeyPath, _) = self.getStateKeyPath(methodName)
                    if let actionArgs = self.value(forKey: handledKeyPath) {
                        let valueType = getAnyValueType(actionArgs)
                        if args.count == 0 {
                            arg = getAnyValue(by: valueType, and: "")
                        } else if valueType.starts(with: "Swift.Dictionary") {
                            arg = args
                        } else if args.count == 1 {
                            arg = getAnyValue(by: valueType, and: args.allValues[0])
                        } else {
                            fatalError("\(methodName) not support args: \(args)")
                        }
                    }
                    self.setValue(arg, forKey: handledKeyPath)
                    return nil
                default:
                    return nil
                }
            }
            return nil
        }
        self.scriptsInvoker.registerInvocation(sdkInvocation)
    }
    
    func fetchToken() -> IOMonad<String> {
        var signal: IOMonad<String>! = nil
        if self.mainStates.tokenType == .static {
            signal = just(self.mainStates.token ?? "")
        } else if self.mainStates.tokenType != .static && self.mainStates.channelId.count > 0 {
            let urlStr = self.mainStates.tokenType.getFetchURL()
            let keySymbol = self.mainStates.tokenType == .token2 ? "appid" : "key"
            var parameters: [String : Any] = ["channelname": self.mainStates.channelId,
                                              keySymbol: self.mainStates.appId.rawValue,
                                              "uid": "\(self.mainStates.localUID)"]
            var sign = ""
            if self.mainStates.channelProfile == .communication {
                sign = "a5c832fbb6e4473a97df9300eb18eccd"
            } else if self.mainStates.channelProfile == .liveBroadcasting {
                sign = "8c85123d5e8c43c88da748c2141573a7"
            }
            if sign.count > 0 {
                let signSymbol = self.mainStates.tokenType == .token2 ? "appcert" : "sign"
                parameters[signSymbol] = sign
            }
            if self.mainStates.tokenType == .token
                || self.mainStates.tokenType == .token2 {
                parameters["audio"] = self.settings.validAudioTime.rawValue
                parameters["video"] = self.settings.validVideoTime.rawValue
                parameters["data"] = self.settings.validDataTime.rawValue
                parameters["join_channel"] = self.settings.validJoinTime.rawValue
            }
            signal = requestGet(from: urlStr, parameters: parameters).flattenMap { (data, _) -> IOMonad<String> in
                if let token = String(data: data, encoding: String.Encoding.utf8) {
                    self.mainStates.$token.update(value: token, self.localStates)
                    return just(token)
                } else {
                    return error(SimpleError.stringException("Can't decode token from response data."), type: String.self)
                }
            }
        }
        return signal
    }
}

// REMARK: implement engine delegate

extension HermesContext : ScriptEngineDispatch {
    // REMARK: callbacks of block and delegate only be choosed one
    func responseDelegate(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any],
                          isSDKInstance: Bool, instanceClass: String) {
        if let mapping = self.scriptsInvoker.findCallAPIMappingStates(selector) {
            // for set cell.XXX states
            var currStreamId: UInt = 0
            for (stateKey, mappingKey) in mapping {
                let (_, handledKeyPath, cellResult) = self.getStateKeyPath(stateKey)
                if let value = parseCellMappingStreamId(mappingKey, argValues) {
                    currStreamId = value
                } else if let value = parseCustomStateValue(mappingKey) {
                    self.setContextOrCellState(value, handledKeyPath, cellResult, currStreamId)
                } else if mappingKey == "__void" {
                    self.setContextOrCellState("", handledKeyPath, cellResult, currStreamId)
                } else if let value = argValues[mappingKey] {
                    self.setContextOrCellState(value, handledKeyPath, cellResult, currStreamId)
                }
            }
        }
    }
    
    func responseBlock(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any],
                       isSDKInstance: Bool, instanceClass: String) {
        if let mapping = self.scriptsInvoker.findCallAPIMappingStates(selector) {
            // for set cell.XXX states
            var currStreamId: UInt = 0
            for (stateKey, mappingKey) in mapping {
                let (_, handledKeyPath, cellResult) = self.getStateKeyPath(stateKey)
                if let value = parseCellMappingStreamId(mappingKey, argValues) {
                    currStreamId = value
                } else if let value = parseCustomStateValue(mappingKey) {
                    self.setContextOrCellState(value, handledKeyPath, cellResult, currStreamId)
                } else if mappingKey == "__void" {
                    self.setContextOrCellState("", handledKeyPath, cellResult, currStreamId)
                } else if let value = argValues[mappingKey] {
                    self.setContextOrCellState(value, handledKeyPath, cellResult, currStreamId)
                }
            }
        }
    }
    
    func responseAPI(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any], returnValue: Any,
                     isSDKInstance: Bool, instanceClass: String) {
        var realSelector = selector
        if let callAPICtx = argValues["__ctx"] as? [String : Any],
           let actionName = callAPICtx["actionNameForSameSelector"] as? String {
            realSelector = "\(actionName)@\(selector)"
        }
        if let mapping = self.scriptsInvoker.findCallAPIMappingStates(realSelector) {
            // for set cell.XXX states
            let currStreamId: UInt = parseCellMappingStreamId("", argValues) ?? 0
            for (stateKey, mappingKey) in mapping {
                let (_, handledKeyPath, cellResult) = self.getStateKeyPath(stateKey)
                if let value = parseCustomStateValue(mappingKey) {
                    self.setContextOrCellState(value, handledKeyPath, cellResult, currStreamId)
                } else if mappingKey == "return", let value = argValues["return"] {
                    self.setContextOrCellState(value, handledKeyPath, cellResult, currStreamId)
                } else if mappingKey == "__void" {
                    self.setContextOrCellState("", handledKeyPath, cellResult, currStreamId)
                } else if let value = argValues[mappingKey] {
                    self.setContextOrCellState(value, handledKeyPath, cellResult, currStreamId)
                }
            }
        }
    }
}

// REMARK: handle binding of control and states

func statesBindTo<ControlType: NSView>(_ localStates: StatesStore, keyPath: String, _ control: ControlType,
                                       _ handler: @escaping (ControlType, Any) -> ()) {
    var signal = localStates.subscribeState(byKey: keyPath, sectionName: "")
        .take(until: control.rac_willDealloc)
    if let startValue = localStates.getState(keyPath, sectionName: "") {
        signal = signal.start(with: (startValue, keyPath, ""))
    }
    signal.subscribeNext { arg in
        let val = arg.0
        handler(control, val)
    }
}

func statesSubscribeFrom<ControlType: NSView>(_ localStates: StatesStore, _ engine: ScriptEngine,
                                              keyPath: String, _ control: ControlType,
                                              _ mappingControl: StateMappingControl,
                                              setValueFn: @escaping Action2<Any?, String>) {
    if let select = control as? NSPopUpButton {
        select.rac_eventTouch.subscribeNext { select in
            let currValue = localStates.getState(keyPath, sectionName: "")
            if select.indexOfSelectedItem == 0 && mappingControl.defaultSelectItem != nil {
                setValueFn(nil, keyPath)
            } else if let enumClass = mappingControl.enumClass {
                if let value = getEnumAnyValue(by: select.indexOfSelectedItem, enumClass, engine),
                   !equalsOptionalAny(lhs: currValue, rhsValue: value) {
                    setValueFn(value, keyPath)
                }
            } else if let items = mappingControl.stringItems {
                let value = items[select.indexOfSelectedItem]
                if !equalsOptionalAny(lhs: currValue, rhsValue: value) {
                    setValueFn(value, keyPath)
                }
            }
        }
    } else if let chk = control as? NSButton {
        chk.rac_eventTouch.subscribeNext { chk in
            setValueFn((chk.state == .on ? true : false), keyPath)
        }
    } else if let input = control as? NSTextField {
        input.rac_valueDidChanged.subscribeNext { inputValue in
            if let state = localStates.getState(keyPath, sectionName: "") {
                let stateType = type(of: state)
                if let cc = stateType as? TypeConvertible.Type {
                    if let value = cc.tryConvert(value: inputValue) {
                        setValueFn(value, keyPath)
                    }
                }
            } else {
                if mappingControl.intRange != nil {
                    if let intValue = Int(inputValue) {
                        setValueFn(intValue, keyPath)
                    }
                } else {
                    setValueFn(inputValue, keyPath)
                }
            }
        }
    }
}

// REMARK: RACAssign for HermesContext

fileprivate var KeyPathMapping = [Int : String]()
func findMatchedPropName<Property, Subject>(_ keyPath: KeyPath<Subject, Property>,
                                            _ sectionName: String,
                                            _ allProperties: [String: Any],
                                            _ localStates: StatesStore,
                                            subject: Subject) -> String? {
    let propType = Property.self
    let propTypeStr = String(reflecting: propType)
    if let propName = KeyPathMapping[keyPath.hashValue] {
        return propName
    } else {
        var propName: String? = nil
        if allProperties.count > 1 {
            let propType = "\(Property.self)"
            let testOriginValue = subject[keyPath: keyPath]
            // get testValue for RawEnumConvertible
            let rawType = Property.self as? RawEnumConvertible.Type
            
            var enumTestValue: Any? = nil
            var findEnumTestValue = false
            if let rawValue = testOriginValue as? RawEnumConvertible,
               let engine = localStates.getState("engine", sectionName: "") as? ScriptEngine {
                if let testValue0 = getEnumAnyValue(by: 0, propType, engine) {
                    if !equalsAny(lhs: rawValue.anyRawValue, rhs: testValue0) {
                        findEnumTestValue = true
                        enumTestValue = testValue0
                    }
                }
                if !findEnumTestValue, let testValue1 = getEnumAnyValue(by: 1, propType, engine) {
                    if !equalsAny(lhs: rawValue.anyRawValue, rhs: testValue1) {
                        findEnumTestValue = true
                        enumTestValue = testValue1
                    }
                }
            }
            for (key, value) in allProperties {
                // detect RawEnumConvertible prop's value
                if findEnumTestValue, let rawType = rawType, let rawValue = testOriginValue as? RawEnumConvertible {
                    var canDetect = false
                    if let enumCase = value as? RawEnumConvertible, propType == "\(type(of: value))",
                       equalsAny(lhs: enumCase.anyRawValue, rhs: rawValue.anyRawValue) {
                        canDetect = true
                    } else if rawType.createInstance(by: value) != nil,
                              equalsAny(lhs: value, rhs: rawValue.anyRawValue) {
                        canDetect = true
                    }
                    if canDetect, let enumTestValue = enumTestValue {
                        // set detect-value
                        localStates.setState(enumTestValue, byKey: key, sectionName: sectionName)
                        // get test-value
                        let testCurrValue = subject[keyPath: keyPath]
                        // reset origin value
                        localStates.setState(value, byKey: key, sectionName: sectionName)
                        if let currCaseValue = testCurrValue as? RawEnumConvertible,
                           equalsAny(lhs: currCaseValue.anyRawValue, rhs: enumTestValue) {
                            KeyPathMapping[keyPath.hashValue] = key
                            return key
                        }
                    }
                    continue
                }
                let valueTypeStr = getAnyValueType(value)
                let valueOptionalTypeStr = "Swift.Optional<\(valueTypeStr)>"
                // set test-value
                if (valueTypeStr == propTypeStr || valueOptionalTypeStr == propTypeStr), let generic = value as? Genericable {
                    generic.setDetectPropValue(instance: localStates, keyPath: key, sectionName: sectionName)
                    // get test-value
                    let testCurrValue = subject[keyPath: keyPath]
                    if let valueBool = value as? Bool {
                        if equalsAny(lhs: testCurrValue as Any, rhs: !valueBool)
                            && "\(testCurrValue)" != "\(testOriginValue)" {
                            propName = key
                        }
                    } else if let type = Property.self as? Genericable.Type,
                              equalsAny(lhs: testCurrValue, rhs: type.detectPropValue()) {
                        propName = key
                    }
                    // reset origin value
                    localStates.setState(value, byKey: key, sectionName: sectionName)
                    if propName != nil {
                        break
                    }
                }
            }
        } else {
            propName = allProperties.keys.first
        }
        if let propName = propName {
            KeyPathMapping[keyPath.hashValue] = propName
            return propName
        } else {
            return nil
        }
    }
}

extension RACAssign where Subject: RepositoriesStates {
    subscript<Property>(dynamicMember keyPath: KeyPath<Subject, Property>) -> IOMonad<Property> {
        get {
            fatalError("cannot read from this object.")
        }
        set(stream) {
            let localStates = HermesContext.states
            if let propName = findMatchedPropName(keyPath, self.subject.sectionName,
                                                  self.subject.toDictionary(),
                                                  localStates, subject: self.subject) {
                stream.take(until: self.subject.rac_willDealloc).subscribeNext { val in
                    localStates.updateState(val, byKey: propName, sectionName: self.subject.sectionName)
                }
            } else {
                fatalError("not found \(keyPath) mapping propName.")
            }
        }
    }
    
    subscript<Property>(dynamicMember keyPath: KeyPath<Subject, Property?>) -> IOMonad<Property> {
        get {
            fatalError("cannot read from this object.")
        }
        set(stream) {
            let localStates = HermesContext.states
            if let propName = findMatchedPropName(keyPath, self.subject.sectionName,
                                                  self.subject.toDictionary(),
                                                  localStates, subject: self.subject) {
                stream.take(until: self.subject.rac_willDealloc).subscribeNext { val in
                    localStates.updateState(val, byKey: propName, sectionName: self.subject.sectionName)
                }
            } else {
                fatalError("not found \(keyPath) mapping propName.")
            }
        }
    }
    
    subscript<Property: ComplexCaseIterable>(dynamicMember keyPath: KeyPath<Subject, Property>) -> IOMonad<ComplexEnumIndex> {
        get {
            fatalError("cannot read from this object.")
        }
        set(stream) {
            let localStates = HermesContext.states
            if let propName = findMatchedPropName(keyPath, self.subject.sectionName,
                                                  self.subject.toDictionary(),
                                                  localStates, subject: self.subject) {
                // TODO: check，不加weak是否有能存泄漏
                stream.take(until: self.subject.rac_willDealloc).subscribeNext { index in
                    let complexEnumType = Property.self
                    if let engine = localStates.getState("engine", sectionName: "") as? ScriptEngine,
                       let value = getEnumAnyValue(index: index, "\(complexEnumType)", engine) {
                        localStates.updateState(value, byKey: propName, sectionName: self.subject.sectionName)
                    }
                }
            } else {
                fatalError("not found \(keyPath) mapping propName.")
            }
        }
    }
    
    subscript<Property: ComplexCaseIterable>(dynamicMember keyPath: KeyPath<Subject, Property?>) -> IOMonad<ComplexEnumIndex> {
        get {
            fatalError("cannot read from this object.")
        }
        set(stream) {
            let localStates = HermesContext.states
            if let propName = findMatchedPropName(keyPath, self.subject.sectionName,
                                                  self.subject.toDictionary(),
                                                  localStates, subject: self.subject) {
                stream.take(until: self.subject.rac_willDealloc).subscribeNext { index in
                    let complexEnumType = Property.self
                    if let engine = localStates.getState("engine", sectionName: "") as? ScriptEngine,
                       let value = getEnumAnyValue(index: index, "\(complexEnumType)", engine) {
                        localStates.updateState(value, byKey: propName, sectionName: self.subject.sectionName)
                    }
                }
            } else {
                fatalError("not found \(keyPath) mapping propName.")
            }
        }
    }
}

// REMARK: responds actions

fileprivate func onRespondsScriptAction(_ actionName: String, context: HermesContext) {
    if context.scriptsInvoker.runningMode != .notStart {
        var ctx = [String : Any]()
        var realName = actionName
        // handle cell action
        if let result = parseCellActionName(actionName), context.mainStates.streamsInfo.contains(result.2) {
            realName = result.0
            ctx[result.1] = ""
        }
        context.mainStates.$showCallActionLoadingTrigger.update(value: true, HermesContext.states)
        context.scriptsInvoker.callCustomAction(realName, instrContext: ctx) {[weak context] in
            if let self = context {
                self.mainStates.$showCallActionLoadingTrigger.update(value: false, HermesContext.states)
            }
        }
    } else {
        FLShowCloseAlert("Please run the script first.")
    }
}

fileprivate func onEngineStartRunning(_ sdkInstance: Any, context: HermesContext) {
    if sdkInstance is AgoraRtcEngineKit {
        context.engine.running(with: sdkInstance as AnyObject)
        context.$isEngineRunning.update(value: true, HermesContext.states)
    }
}

fileprivate func onEngineStop(_ arg: Any, context: HermesContext) {
    context.$isEngineRunning.update(value: false, HermesContext.states)
    context.engine.stop()
}

fileprivate func onSetupLocalStream(_ uid: UInt, context: HermesContext) {
    context.mainStates.streamsInfo.addStream(uid, forLocal: true)
    context.mainStates.$handleStreamTrigger.update(value: (uid, false, true), HermesContext.states)
}

fileprivate func onSetupRemoteStream(_ uid: UInt, context: HermesContext) {
    context.mainStates.streamsInfo.addStream(uid, forLocal: false)
    context.mainStates.$handleStreamTrigger.update(value: (uid, true, true), HermesContext.states)
}

fileprivate func onRemoveLocalStream(_ arg: Any, context: HermesContext) {
    let localUID = context.mainStates.streamsInfo.localStreamId
    context.mainStates.$handleStreamTrigger.update(value: (localUID, false, false), HermesContext.states)
    context.mainStates.streamsInfo.removeStream(localUID)
}

fileprivate func onRemoveAllRemoteStreams(_ arg: Any, context: HermesContext) {
    context.mainStates.$handleStreamTrigger.update(value: (UInt.max, true, false), HermesContext.states)
    context.mainStates.streamsInfo.removeAllRemoteStreams()
}

fileprivate func onRemoveRemoteStream(_ uid: UInt, context: HermesContext) {
    context.mainStates.$handleStreamTrigger.update(value: (uid, true, false), HermesContext.states)
    context.mainStates.streamsInfo.removeStream(uid)
}

fileprivate func onHandleStartCapture(_ arg: String, context: HermesContext) {
    if arg == "close" {
        context.mainStates.$captureSharing.update(value: false, HermesContext.states)
        context.mainStates.$showScreenCaptureTrigger.update(value: (false, nil), HermesContext.states)
    } else {
        let forUpdateParams = context.mainStates.captureSharing
        context.mainStates.$showScreenCaptureTrigger.update(value: (true, forUpdateParams ? false : nil), HermesContext.states)
    }
}
